/**
 * Not type-checking this file because it's mostly vendor code.
 */

/*!
 * HTML Parser By John Resig (ejohn.org)
 * Modified by Juriy "kangax" Zaytsev
 * Original code by Erik Arvidsson (MPL-1.1 OR Apache-2.0 OR GPL-2.0-or-later)
 * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
 */

import { makeMap, no } from 'shared/util'
import { isNonPhrasingTag } from 'web/compiler/util'
import { unicodeRegExp } from 'core/util/lang'

// Regular Expressions for parsing tags and attributes
//匹配标签的属性, 在html中，有四种书写属性的方式
// 1、使用双引号把值引起来：`class="some-class"`
// 2、使用单引号把值引起来：`class='some-class'`
// 3、不使用引号：`class=some-class`
// 4、单独的属性名：`disabled`
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/

//匹配动态属性比如
//:name='张三'
//v-bind:name='张三'
//@click='event'
//v-on:click='event'
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/

//匹配一些特殊规则的命名
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`

//匹配： 前缀+ 冒号 + 名称 这种格式，然后匹配到名称
const qnameCapture = `((?:${ncname}\\:)?${ncname})`

//匹配< + qnameCapture的规则
const startTagOpen = new RegExp(`^<${qnameCapture}`)

//匹配: >  />
const startTagClose = /^\s*(\/?)>/

//匹配结束标签
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)

//这个正则用来匹配文档的 `DOCTYPE` 标签，没有捕获组
const doctype = /^<!DOCTYPE [^>]+>/i

// #7298: escape - to avoid being passed as HTML comment when inlined in page
//避免在页面内联时作为HTML注释传递
const comment = /^<!\--/

const conditionalComment = /^<!\[/

// 检测给定的标签名字是不是纯文本标签
export const isPlainTextElement = makeMap('script,style,textarea', true)
const reCache = {}

//特殊字符解码
const decodingMap = {
  '&lt;': '<',
  '&gt;': '>',
  '&quot;': '"',
  '&amp;': '&',
  '&#10;': '\n',
  '&#9;': '\t',
  '&#39;': "'"
}
const encodedAttr = /&(?:lt|gt|quot|amp|#39);/g
const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#39|#10|#9);/g

//这段代码主要是解决了一个怪癖行为，就是<pre>标签会忽略开头的第一个换行就是你在写代码的时候
//<pre>
//内容</pre>  浏览器会把你的换行符给干掉，等价于你写的<pre>内容</pre>, 这里是vue帮我们把这个换行符号换上了
const isIgnoreNewlineTag = makeMap('pre,textarea', true)
const shouldIgnoreFirstNewline = (tag, html) => tag && isIgnoreNewlineTag(tag) && html[0] === '\n'

//这里就是将匹配到的特殊字符替换为上面解码器的内容
function decodeAttr(value, shouldDecodeNewlines) {
  const re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr
  return value.replace(re, match => decodingMap[match])
}



export function parseHTML(html, options) {

  const stack = []  //存储所有标签的数组
  const expectHTML = options.expectHTML  //boolean: true
  const isUnaryTag = options.isUnaryTag || no //传入str判断是否是特定的标签isUnaryTag,  返回boolean
  const canBeLeftOpenTag = options.canBeLeftOpenTag || no //传入str判断是否是特定的标签canBeLeftOpenTag,  返回boolean
  let index = 0 //解析到内容的索引位置
  let last; //剩下的未解析的内容
  let lastTag //解析到内容是标签，不带括号，动态的，主要提供给下一次循环用的，判断是不是闭合了

  //一个while循环不断其解析字符串，它的解析过程是一段一段的解析的
  while (html) {
    last = html
    
    //确保我们不是在像script/style这样的纯文本内容元素中
    if (!lastTag || !isPlainTextElement(lastTag)) {
      let textEnd = html.indexOf('<')
      if (textEnd === 0) {
        // Comment:  有可能是注释节点
        if (comment.test(html)) {
          const commentEnd = html.indexOf('-->')

          if (commentEnd >= 0) {
            if (options.shouldKeepComment) {
              //html.substring(4, commentEnd)获取到了注释的内容
              options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
            }
            //剔除掉剩余的没用部分
            advance(commentEnd + 3)
            continue
          }
        }

        // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
        // 有可能是条件注释节点
        if (conditionalComment.test(html)) {
          const conditionalEnd = html.indexOf(']>')

          if (conditionalEnd >= 0) {
            advance(conditionalEnd + 2)
            continue
          }
        }

        // Doctype: doctype 节点
        const doctypeMatch = html.match(doctype)
        if (doctypeMatch) {
          advance(doctypeMatch[0].length)
          continue
        }

        // End tag: 结束标签
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          const curIndex = index
          advance(endTagMatch[0].length)
          //解析结束标签,如</span>那么，endTagMatch[1]=span, curIndex就是<的索引位置，index 就是>的索引位置
          parseEndTag(endTagMatch[1], curIndex, index)
          continue
        }

        // Start tag: 开始标签
        const startTagMatch = parseStartTag()
        if (startTagMatch) {
          handleStartTag(startTagMatch)
          if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
            advance(1)
          }
          continue
        }
      }

      /**
       * 这里解析的内容是开始标签和结束标签之间的内容，分几种情况
       * 1.可能是空字符串
       * 2.可能是单行注释
       * 3.可能是多行注释
       * 4.可能是字符串内容
       * 5.也有可能是另一个标签的开始
       */
      let text, rest, next //text表示的文本内容，也有可能是一个不为空的字符串
      if (textEnd >= 0) {
        //去掉多余的空格，获取内容
        rest = html.slice(textEnd)
        while (
          !endTag.test(rest) && //是否符合结束标签</
          !startTagOpen.test(rest) && //是否符合开始标签<
          !comment.test(rest) && //是否是多行注释
          !conditionalComment.test(rest) //是否是当行注释
        ) {
          // 一直循环知道获取到<括号位置的索引，然后结束循环，开始解析内容到<为止的内容
          next = rest.indexOf('<', 1)
          if (next < 0) break //解析到是另一个标签是开始，直接停止当前本次循环开始进入下一次while
          textEnd += next
          rest = html.slice(textEnd)
        }
        text = html.substring(0, textEnd)
      }

      if (textEnd < 0) {
        text = html
      }

      //获取到需要解析的内容，然后设置一下步进结束的index和剩余的解析内容last
      if (text) {
        advance(text.length)
      }
      //开始解析字符串内容
      if (options.chars && text) {
        //参数一： 字符串的内容
        //参数二： 开始的索引
        //参数三： 结束的索引
        options.chars(text, index - text.length, index)
      }
    } else {
      let endTagLength = 0
      const stackedTag = lastTag.toLowerCase()
      const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
      const rest = html.replace(reStackedTag, function (all, text, endTag) {
        endTagLength = endTag.length
        if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
          text = text
            .replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
            .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
        }
        if (shouldIgnoreFirstNewline(stackedTag, text)) {
          text = text.slice(1)
        }
        if (options.chars) {
          options.chars(text)
        }
        return ''
      })
      index += html.length - rest.length
      html = rest
      parseEndTag(stackedTag, index - endTagLength, index)
    }

    if (html === last) {
      options.chars && options.chars(html)
      if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {
        options.warn(`Mal-formatted tag at end of template: "${html}"`, { start: index + html.length })
      }
      break
    }
  }

  // Clean up any remaining tags
  parseEndTag()

  //改变index的索引和截取掉html的内容，算是给下一次while循环的时候就可以提高性能
  function advance(n) {
    index += n
    html = html.substring(n)
  }

  /**
   * 获取开头标签, 一个小知识，数组和函数都是可以为其添加属性的，如 var arr = [] ,arr.name='张三'
   * 如输入内容(没有开头括号):  div id="app">....子类内容</div>
   * 那么返回值就是：{ 
   *    attr: [ 
   *       ["id=\"app\"", "id", "=", "app", undefined, undefined, 属性：start: 4, end: 13, index: 0,  ]
   *    ], 
   *    end: 14, 
   *    start: 0, 
   *    tagName: "div", 
   *    unarySlash: "" 
   * }
   */
  function parseStartTag() {
    const start = html.match(startTagOpen)
    if (start) {
      const match = {
        tagName: start[1],
        attrs: [],
        start: index
      }
      advance(start[0].length)
      let end, attr
      //匹配属性值一个循环匹配到所有的attribute，然后就是根据">"得到结束的索引
      while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
        attr.start = index
        advance(attr[0].length)
        attr.end = index
        match.attrs.push(attr)
      }
      if (end) {
        match.unarySlash = end[1]
        advance(end[0].length)
        match.end = index
        return match
      }
    }
  }

  /**
   * 这个方法的作用就是获取到lastTag和将内容推进去stack
   * 如输入上面parseStartTag方法的示例内容，那么得到的结果是
   * lastTag = 'div'
   * stack: [ { tag: 'div', lowerCasedTag: 'div' start: 0, end: 14 , attrs: [ { name: 'id', value: 'app', start: 5, end: 13 } ]} ]
   * @param {} match parseStartTag的返回值
   */
  function handleStartTag(match) {
    const tagName = match.tagName
    const unarySlash = match.unarySlash

    if (expectHTML) {
      if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
        parseEndTag(lastTag)
      }
      if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
        parseEndTag(tagName)
      }
    }

    //是否是特定的标签isUnaryTag
    const unary = isUnaryTag(tagName) || !!unarySlash

    const l = match.attrs.length
    const attrs = new Array(l)
    for (let i = 0; i < l; i++) {
      const args = match.attrs[i]
      const value = args[3] || args[4] || args[5] || ''
      const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
        ? options.shouldDecodeNewlinesForHref
        : options.shouldDecodeNewlines
      attrs[i] = {
        name: args[1],
        value: decodeAttr(value, shouldDecodeNewlines)
      }
      if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
        attrs[i].start = args.start + args[0].match(/^\s*/).length
        attrs[i].end = args.end
      }
    }

    if (!unary) {
      stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
      lastTag = tagName
    }

    /**
     * start方法在./index.js里面传递进来的
     */
    if (options.start) {
      /**
       * 这里做了几件事，处理input标签，处理v-pre，v-if, v-else ,v-else-if, v-for指令
       */
      options.start(tagName, attrs, unary, match.start, match.end)
    }
  }

  function parseEndTag(tagName, start, end) {
    let pos, lowerCasedTagName
    if (start == null) start = index
    if (end == null) end = index

    // Find the closest opened tag of the same type
    if (tagName) {
      lowerCasedTagName = tagName.toLowerCase()
      //循环数组，找到等于当前闭合标签的元素，返回索引pos
      for (pos = stack.length - 1; pos >= 0; pos--) {
        if (stack[pos].lowerCasedTag === lowerCasedTagName) {
          break
        }
      }
    } else {
      // If no tag name is provided, clean shop
      pos = 0
    }

    if (pos >= 0) {
      // Close all the open elements, up the stack
      for (let i = stack.length - 1; i >= pos; i--) {
        if (process.env.NODE_ENV !== 'production' &&
          (i > pos || !tagName) &&
          options.warn
        ) {
          options.warn(
            `tag <${stack[i].tag}> has no matching end tag.`,
            { start: stack[i].start, end: stack[i].end }
          )
        }

        
        if (options.end) {
          options.end(stack[i].tag, start, end)
        }
      }

      // Remove the open elements from the stack
      stack.length = pos
      lastTag = pos && stack[pos - 1].tag
    } else if (lowerCasedTagName === 'br') {
      if (options.start) {
        options.start(tagName, [], true, start, end)
      }
    } else if (lowerCasedTagName === 'p') {
      if (options.start) {
        options.start(tagName, [], false, start, end)
      }
      if (options.end) {
        options.end(tagName, start, end)
      }
    }
  }
}
